[Previous] [Next]

Multithreaded ActiveX Components

Both Visual Basic 5 and 6 can create multithreaded ActiveX components. Components built with the first release of Visual Basic 5, however, could only support multithreading if they had no user interface, which is a serious limitation in some cases. This restriction was lifted in Service Pack 2.

Threading Models

In a nutshell, multithreading is the ability to execute different code portions of an application at the same time. Many popular Windows applications are multithreaded. For example, Microsoft Word uses at least two threads, and the Visual Basic environment uses five threads. Multiple threads are a good choice when you need to execute complex tasks in the background (for example, paginating a document) or when you want to keep the user interface responsive even when your application is doing something else. Multiple threads are especially necessary when you're building scalable remote components that have to serve hundreds of clients at the same time.

There are two main types of threading models: free threading and apartment threading. In the free-threading model, each thread can access the entire process's data area and all threads share the application's global variables. Free threading is powerful and efficient, but it's a nightmare even for most experienced programmers because you must arbitrate among all the shared resources, including variables. For example, even an innocent statement such as

If x > 1 Then x = x - 1    ' X should always be greater than 1.

can create problems. Imagine this scenario: Thread A reads the value of the x variable and finds that it is 2, but before it executes the Then portion of the statement, the CPU switches to Thread B. Thread B happens to be executing the same statement (an unlikely but not impossible circumstance), finds that x is 2, and therefore decrements it to 1. When Thread A regains the control of the CPU, it decrements the variable to 0, which is an invalid value that will probably cause other logic errors later in the program's life.

The apartment-threading model solves these problems by encapsulating each thread in an apartment. Code executed in a given apartment can't access variables belonging to other apartments. Each apartment has its own set of variables, so if two threads are accessing the x variable at the same time, they're actually referencing two different memory locations. This mechanism neatly solves the synchronization problem described earlier, and for this reason the apartment-threading model is inherently safer than the free-threading model. In Visual Basic, you can build ActiveX components that support the apartment model only.

Multithreaded ActiveX EXE Components

Visual Basic 5 and 6 let you create out-of-process servers that create an additional thread when a client instantiates a new object. All you need to do to transform a regular ActiveX EXE component into a multithreaded component is select an option in the General tab of the Project Properties dialog box. (See Figure 16-18.) There are three possible settings. The default setting is the Thread Pool option with 1 thread; this corresponds to a single-threaded component.

Figure 16-18. Create a multithreaded component with a few mouse clicks in the Project Properties dialog box.

If you select the Thread Per Object option, you build a multithreaded component that creates a new thread for every object requested by its clients. Because all objects are executed in their own threads, no client can ever block another client, so these components are highly responsive. The disadvantage of this approach is that too many threads can bring even a powerful system to its knees because Windows has to spend a lot of time just switching from one thread to the other.

Thread pools

If you select the Thread Pool option and enter a value greater than 1 in the Threads field, you build a multithreaded component that's allowed to create only a limited number of threads. This is a scalable solution in the sense that you can increase the size of the thread pool when you deploy your application on a more powerful system. (You need to recompile the application, though.) This setting prevents the system from wasting too much time on thread management because the pool can't grow larger than the limit you set. To assign threads to objects, the pool uses a round robin algorithm, which always tries to assign the first available thread to each new request for an object.

Let's say that you created a multithreaded component with a pool size of 10 threads. When the first client makes a request for an object, COM loads the component, which returns the created object in its first thread. When a second client makes a request, the component creates an object in a second thread, and so on, until the tenth client gets the last available thread in the pool. When the eleventh request comes, the component has to return an object in one of the threads that have been created previously. The thread used for this new object can't be determined in advance because it depends on several factors. For this reason, the round robin algorithm is said to be a nondeterministic algorithm.

Here are a few interesting points that concern object pooling. First, when there are more objects than threads, each thread can serve more objects, possibly owned by different clients. In this situation, a given thread can't execute an object's method if it's already serving another object. In other words, an object pool doesn't completely prevent objects from blocking one another (as components with one thread per object do), even if this problem happens less frequently than with single-threaded components.

Second, once an object has been created in a thread, it must execute in that thread; this is a requirement of apartment threading. Therefore, a client might be blocked by another client even if the component has some unallocated threads. Imagine this scenario: You have a pool with 10 threads, and you instantiate 20 objects. In an ideal situation, the pool is perfectly balanced and each thread serves exactly two objects. But suppose that all the objects served by threads 1 through 9 are released while the two objects served by thread 10 aren't. In this case, the pool has become highly unbalanced and the two objects will block each other, even if the pool has nine available threads.

Finally, even if the apartment model ensures that all apartments have a different set of variables, objects in the same thread share the same apartment and therefore share the same global values. This might appear to be a cheap way to exchange data among objects, but in practice you can't use this technique because you can't predict which objects will share the same thread.

The multithreading advantage

Many programmers mistakenly believe that multithreading is always a good thing. The truth, however, is that most computers have only one CPU, which has to execute all the threads in all the processes in the system. Multithreading is always a good thing if you're executing your component on a Windows NT machine with multiple CPUs; in this situation, the operating system automatically takes advantage of the additional processors to balance the workload. In the most common case, however, you're working with a single-processor machine and you might find that multithreading can even make your performance worse. This is a somewhat counter-intuitive concept, so I'll explain it with an example.

Let's say that you have two threads that execute two different tasks, each one taking 10 seconds to complete. In a single-threaded environment, one of the two tasks completes in 10 seconds, and the other waits for the first one to complete and therefore takes 20 seconds in total. The result is that the average time is 15 seconds per task. In a multithreaded environment, the two tasks would execute in parallel and will complete more or less at the same time. Unless you have two CPUs, in this case the average time is 20 seconds, which is worse than in the single-threaded case.

In summary, multithreading isn't always the best solution. Sometimes, however, it clearly offers advantages over single-threading:

When you're deciding between single- and multithreading, don't forget that Visual Basic applications implicitly use multithreading for some tasks—for example, when printing data. Moreover, some database engines (most notably, the Microsoft Jet engine) internally use multithreading.

User-interface issues

Visual Basic 6 lets you create multithreaded components that expose a user interface. (You need Service Pack 2 to have this feature work under Visual Basic 5.) You can achieve this because all the forms and the ActiveX controls that you create are thread safe, which means that multiple instances of them can independently execute in different threads. The same is true for ActiveX documents and designers, such as the DataEnvironment designer, as well as the majority of the ActiveX controls that are in the package—for example, the MaskEdBox control and all the Windows common controls.

But a few ActiveX controls are inherently single-threaded and can't be safely used inside multithreaded components—for example, the Microsoft Chart (MSCHRT20.OCX) and Microsoft Data Bound Grid (DBGRID32.OCX) controls. If you attempt to add these controls to an ActiveX DLL project whose threading model is Apartment Threaded or to an ActiveX EXE project whose threading model is Thread Per Object or Thread Pool with a number of threads greater than 1, you get an error and the control isn't added to the Toolbox. You also get an error if you have a project that already includes one or more single-threaded controls and you change the project type to a value that isn't compatible with such controls. When you buy a third-party control, check with its vendor to learn whether it supports multithreading.

CAUTION
You can force Visual Basic to accept a single-threaded ActiveX control in a multithreaded project by manually editing the VBP file. There are many reasons not to do that, however. Single-threaded controls running in a multithreaded application perform poorly and, above all, can cause many problems and unexpected behavior. For example, the Tab key and Alt+key combinations don't work as they should, and a click on the control might not activate the form. Moreover, there might be some properties (most notably, the Picture property) whose values can't be marshaled between different threads, and any attempt to do so raises a run-time error.

Here are other minor issues concerning forms inside multithreaded components:

Unattended execution

If your component doesn't include a form, UserControl, or UserDocument module, you can tick the Unattended Execution check box in the General tab of the Project Properties dialog box. This indicates that your component is meant to execute without any user interaction, a reasonable option when you're creating a component to run remotely on another machine.

The Unattended Execution option suppresses any message boxes or other kinds of user interface (including error messages) and redirects them to a log file or the Windows NT Application Event Log. You can also send your own messages to this log file. Using this option is important with remote components because any message box would stop the execution of the component until the user closes it, but when a component is running remotely no interactive user can actually close the dialog box.

The StartLogging method of the App object lets you select where your messages will be sent. Its syntax is as follows:

App.StartLogging LogFile, LogMode

where LogFile is the name of the file that will be used for logging, and LogMode is one of the values listed in Table 16-2. The vbLogOverwrite and vbLogThreadID settings can be combined with the other values in the table. When you're sending a message to the Windows NT Application Event Log, "VBRunTime" is used as the application source and the App.Title property appears in the description. When you're running under Windows 95 or 98, messages are sent by default to a file named Vbevents.log.

CAUTION
Watch out for two bugs. First, if you specify an invalid filename, no errors are raised and logged messages silently go to default output. Also, the vbLogOverwrite option makes the StartLogging method behave as if the vbLogAuto option were specified. So you should always manually delete the log file and not rely on the vbLogOverwrite option.

Table 16-2. All the values for the LogMode argument of the App object's StartLogging method; these are also the possible return values of the LogMode read-only property.

Constant Value Description
vbLogAuto 0 If running under Windows 95 or 98, messages are logged to the file specified by the LogFile argument; if running under Windows NT, messages are logged to the Windows NT Application Event Log.
vbLogOff 1 Messages aren't logged anywhere and are simply discarded; message boxes have no effect.
vbLogToFile 2 Forces logging to file, or turns off logging if no valid file name is passed in the LogFile argument. (In the latter case, the LogMode property is set to vbLogOff.)
vbLogToNT 3 Forces logging to the Windows NT Application Event Log; if not running under Windows NT or the Event Log is unavailable, it turns off logging and resets the LogMode property to vbLogOff.
vbLogOverwrite 16 When logging to a file, it re-creates the log file each time the application starts; it has no effect when logging to the Application Event Log. It can be combined with other values in this table using the OR operator.
vbLogThreadID 32 The current thread ID is added to the beginning of the message in the form "[T:0nnn]"; if this value is omitted, the thread ID is shown only if the message comes from a multithreaded application. It can be combined with other values in this table using the OR operator.

Once you have set up logging, you can log messages using the App object's LogEvent method, which has the following syntax:

App.LogEvent LogMessage, EventType

LogMessage is the text of the message, and EventType is an optional argument that states the type of the event (one of the following values: 1-vbLogEventTypeError, 2-vbLogEventTypeWarning, or 4-vbLogEventTypeInformation). For example, the following piece of code

App.StartLogging "C:\Test.Log", vbLogAuto 
App.LogEvent "Application Started", vbLogEventTypeInformation
App.LogEvent "Memory is running low", vbLogEventTypeWarning
App.LogEvent "Unable to find data file", vbLogEventTypeError
MsgBox "Press any key to continue", vbCritical

sends its output to the C:\TEST.LOG file if run under Windows 95 or 98 or to the Application Event Log if run under Windows NT. (See Figure 16-19.)

Click to view at full size.

Figure 16-19. Logged messages coming from a Visual Basic application as they appear in a log text file (top window) or in the Windows NT Application Event Log (bottom window).

You can test the Unattended Execution attribute from code using the read-only UnattendedApp property of the App object. Likewise, you can retrieve the current log file and log mode using the App object's LogPath and LogMode properties, respectively. When you've compiled the code using the Unattended Execution attribute, all the MsgBox commands send their output to the log file or the Windows NT Application Event Log, as if a LogEvent method with the vbLogEventTypeInformation argument were issued.

One last note: If you run the program under the Visual Basic IDE, the Unattended Execution setting has no effect; all message boxes appear on screen as usual, and the App.StartLogging and App.LogEvent methods are ignored. To activate logging, you must compile your application to a stand-alone program.

Multithreaded ActiveX DLL Components

You can also create multithreaded ActiveX DLLs using Visual Basic 6. Unlike ActiveX EXE servers, however, Visual Basic's DLLs can't create new threads and can only use the threads of their clients. For this reason, multithreaded DLLs are most useful with multithreaded client applications. Because an ActiveX DLL doesn't actually create any thread, the options you have in the Project Properties dialog box are simpler than those offered by an ActiveX EXE project. In practice, you only have to decide if you want to create a Single Threaded or Apartment Threaded server. (See Figure 16-20.)

Both single- and multithreaded components are thread safe, which means that when an object in a thread is called by another thread, the calling thread is blocked until the called method returns. This prevents most reentrancy problems and greatly simplifies the job of the programmer.

While it's perfectly safe to use a single-threaded DLL with a multithreaded client, only one thread in the main application can directly call the methods of an object created by the DLL. This particular thread is the first thread created in the client application, or more precisely, the first thread that internally called the OleInitialize function. All the objects exposed by a single-threaded DLL are created in this thread; when they are used from another thread in the client application, arguments and return values undergo the so-called cross-thread marshaling, which is almost as slow as cross-process marshaling.

Figure 16-20. Selecting the Threading Model option in the Project Properties dialog box.

When you don't know how your DLL will be used, selecting an Apartment Threaded option is usually the best choice. In fact, a multithreaded DLL can be used by single-threaded clients without any problem and without any noticeable overhead. In one case a single-threaded DLL can be conveniently used with a multithreaded client, namely, when you want to offer a simple way for all the threads in the client to communicate and share data with each other. An example of this technique is described in the "Testing a Multithreaded Application" section later in this chapter.

Multithreaded Visual Basic Applications

Many programmers aren't aware that Visual Basic can create multithreaded regular applications, not just components. To be honest, creating such multithreaded applications isn't as straightforward as using other Visual Basic advanced features, and you have to account for a number of important issues.

The trick to creating a multithreaded application is simple: The application must be a multithreaded ActiveX EXE server that exposes one or more objects that run in different threads. To build such an application, the conditions shown below must be fulfilled.

When you create an object exposed by the current application using the New operator, Visual Basic uses internal instancing, which bypasses COM and creates the object using a more efficient mechanism that doesn't undergo any restriction. (In fact, you can even create objects from Private or PublicNotCreatable classes.) Conversely, when you use CreateObject, Visual Basic always creates the object through COM. For this reason, the object should be creatable (MultiUse).

Determining the main thread

As I stated previously, the Sub Main procedure in a multithreaded Visual Basic application is executed each time a new thread is created. This isn't usually a problem for multithreaded EXE or DLL components, but it's an issue when you're creating an ActiveX EXE project that must work as a multithreaded application. In this case, it's crucial that you distinguish the first execution from all the subsequent ones: The first time the Main procedure executes, the program must create its main window, whereas in all other cases the procedure shouldn't display any user interface. More precisely, when the procedure is being executed as a result of a request for a new object, it should exit as soon as possible to avoid having the request fail with a timeout error. For the same reason, you should never execute lengthy operations inside the Class_Initialize event procedure.

Understanding whether the Main procedure has never been executed before isn't as trivial a task as it might appear at first. You can't simply use a global variable as a flag because that variable can't be seen from a thread in another apartment. Creating a temporary file in the Main procedure isn't a viable solution either because the application might terminate with a fatal error and never delete the file.

There are at least two ways to solve this problem. The first one is based on the FindWindow API function and is described in the Visual Basic documentation. In the following paragraphs, I'll show you an alternative method, which I believe is less complex and slightly more efficient because it doesn't require that you create a window. This method is based on atom objects, which are sort of global variables managed by the Windows operating system. The Windows API provides functions that let you add a new atom, delete an existing atom, or query for an atom's value.

In the Main procedure of a multithreading application, you test whether a given atom exists. If it doesn't exist, this is the first thread of the application, and you need to create the atom. To have the mechanism work, you must also destroy the atom when you exit the application. This task is ideal for a class that creates the atom in its Class_Initialize procedure and destroys it in its Class_Terminate procedure. Here's the complete source code of the CThread class in the demonstration application on the companion CD:

Private Declare Function FindAtom Lib "kernel32" Alias "FindAtomA" _
    (ByVal atomName As String) As Integer
Private Declare Function AddAtom Lib "kernel32" Alias "AddAtomA" _
    (ByVal atomName As String) As Integer
Private Declare Function DeleteAtom Lib "kernel32" _
    (ByVal atomName As Integer) As Integer
Private atomID As Integer

Private Sub Class_Initialize()
    Dim atomName As String
    ' Build an atom name unique for this instance of the application.
    atomName = App.EXEName & App.hInstance
    ' Create the atom if it doesn't exist already.
    If FindAtom(atomName) = 0 Then atomID = AddAtom(atomName)
End Sub
Private Sub Class_Terminate()
    ' Delete the atom when this thread terminates.
    If atomID Then DeleteAtom atomID
End Sub

Function IsFirstThread() As Boolean
    ' This is the first thread if it was the one which created the atom.
    IsFirstThread = (atomID <> 0)
End Function

The name of the atom is built using the application's name and the instance handle (the App.hInstance property). The latter value is different for each distinct instance of the same application, which ensures that this method works correctly even when the user launches multiple instances of the same executable. The CThread class module exposes only one property, IsFirstThread. The following code shows how you can use this class in a multithreaded application to understand whether it's executing the first thread:

' This is global because it has to live for the entire application's life.
Public Thread As New CThread

Sub Main()
    If Thread.IsFirstThread Then
        ' First thread, refuse to be instantiated as a component.
        If App.StartMode = vbSModeAutomation Then
            Err.Raise 9999, , "Unable to be instantiated as a component"
        End If
        ' Show the user interface.
        frmMainForm.Show
    Else
        ' This is a component instantiated by this same application.
    End If
End Sub

Implementing multithreading

Creating a new thread using the CreateObject function doesn't suffice to actually implement a multithreaded Visual Basic application. In fact, the synchronization mechanism offered by Visual Basic, which usually prevents a series of nasty problems, in this case gets in the way. When the program invokes a method of an object in another thread, the calling thread is blocked until the method returns. So you might have multiple threads, but only one of them is executing at a given time, which obviously isn't what you want.

The easy way to work around this issue is using a Timer control to "awaken" an object in a separate thread after it has returned the control back to the calling thread. You don't need a visible form to achieve this; an invisible form with a Timer control on it can do the job. You can take advantage of the new CallByName function to create a form module that you can easily reuse in all your applications that need this sort of callback mechanism. This is the complete source code of the CCallBack form module that encapsulates this functionality:

Dim m_Obj As Object
Dim m_MethodName As String

Public Sub DelayedCall(obj As Object, Milliseconds As Long, _
    MethodName As String)
    Set m_Obj = obj                       ' Save the arguments.
    m_MethodName = MethodName
    Timer1.Interval = Milliseconds        ' Start the timer.
    Timer1.Enabled = True
End Sub

Private Sub Timer1_Timer()
    Timer1.Enabled = False                ' We need just one call.
    Unload Me
    CallByName m_Obj, m_MethodName, VbMethod     ' Do the callback.
End Sub

The CCallBack form can be used as a class module in other portions of the application. On the companion CD, you'll find a sample multithreaded application that creates and displays multiple count-down forms. (See Figure 16-21.) This is a partial listing of the class that the main application instantiates when it needs to create a new count-down form in a separate thread. (The statements that implement the callback mechanism are in boldface.)

Private Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

Dim frm As frmCountDown
Dim m_Counter As Integer

' The Counter property. Values > 0 display the form and start
' the countdown.
Property Get Counter() As Integer
    Counter = m_Counter
End Property
Property Let Counter(newValue As Integer)
    Dim cbk As New CCallBack
    m_Counter = newValue
    cbk.DelayedCall Me, 50, "Start"
End Property

Sub Start()
    Static active As Boolean
    If active Then Exit Sub             ' Prevent reentrancy.
    active = True
    ' The code that shows the countdown form (omitted...)
    ' ...
    active = False
End Sub

Click to view at full size.

Figure 16-21. The sample countdown multithreading application. Note that each window shows a different thread ID in its caption.

This is the code in the main form of the count-down sample application:

Private Sub cmdStart_Click()
    Dim x As CCountDown
    ' Create a new CCountDown object in another thread.
    Set x = CreateObject("MThrApp.CCountDown")
    ' Set the counter using the value currently in the TextBox control.
    x.Counter = Val(txtSeconds)
    Set x = Nothing
    Beep
End Sub

CAUTION
There's an undocumented detail in the way Visual Basic implements multithreading that deserves your attention. If the client code sets the last reference to the object to Nothing either explicitly or implicitly while the object is executing some code, the client has to wait until the routine in the object terminates. This is far from being irrelevant. For example, if you delete the Set x = Nothing statement in the previous code routine, the x variable will be set to Nothing after the Beep statement when the object has already been awakened by the callback procedure and is currently executing the count-down code. This means that the client has to wait as long as 10 seconds until the object can be completely destroyed, and during that time the main form can't react to the user's actions. You can choose from two ways to work around this problem:  

Testing a multithreaded application

Debugging a multithreaded application or component isn't as simple as testing a regular program. For one thing, you have to compile your application as a stand-alone EXE file because the Visual Basic IDE supports only single-threaded applications and components. This means that you have to forgo all the amenities offered by the environment, including breakpoints, the Watch window, the Locals window, and the step-by-step trace capabilities. For this reason, you should thoroughly test the logic of your application in the environment before turning to multithreading.

When testing a compiled multithreaded application, you must devise alternate debugging strategies. For example, since you can't write values to the Immediate window using Debug.Print methods, you have to resort to logging to file or use plain MsgBox commands. One good idea is to display the thread ID in your messages so that you can learn which particular thread is issuing them:

MsgBox "Executing the Eval proc", vbInformation, "Thread: " & App.ThreadID

Single-threaded ActiveX DLL servers offer a better solution to this problem. As you might remember, you can safely use single-threaded DLLs with multithreaded clients, be they stand-alone applications or other components. For example, you can implement a DLL that exposes a CLog object that gathers trace information from its clients and redirects it to a window. Implementing such a DLL isn't difficult. Here's the source code of the CLog class. (The demonstration application found on the companion CD includes the complete version with additional capabilities.)

' If this property is nonzero, the ThreadID is added to the message.
Public ThreadID As Long

Sub StartLogging(LogFile As String, LogMode As Integer)
    ' Note that this refers to the global hidden form reference.
    ' This form will therefore be shared by all the instances
    ' of the class.
    frmLog.Show
End Sub

Sub LogEvent(ByVal LogText As String)
    If ThreadID Then
        LogText = "[" & Hex$(ThreadID) & "] " & LogText
    End If
    frmLog.LogText.SelStart = Len(frmLog.LogText.Text)
    frmLog.LogText.SelText = LogText & vbCrLf
End Sub

The frmLog form belongs to the ActiveX DLL project and includes the txtLog TextBox control that displays the text messages coming from the multithreaded client application, a CheckBox control that lets the user activate and deactivate the logging, and a CommandButton control that clears the contents of the txtLog control. Figure 1622 shows a new version of the sample multithreaded application that has been enhanced with trace capabilities. The revised code in the main BAS module of the application is shown below.

Public Log As New CLog

Sub Main()
    Log.StartLogging "", 0            ' Initialize the CLog object.
    Log.ThreadID = App.ThreadID
    Log.LogEvent "Entering Sub Main"
    ' Here is the code that displays the main form (omitted...)
    ' ...
    Log.LogEvent "Exiting Sub Main"
End Sub

Click to view at full size.

Figure 16-22. Adding trace capabilities to a multithreaded application using a single-threaded ActiveX DLL. Each message includes the ID of the thread that sent it.

For a more sophisticated test session, you might want to tick the Create Symbolic Debug Info option in the Compile tab of the Project Properties dialog box, and then recompile the application and execute it under a real debugger, such as the one included in Microsoft Visual C++.